/* src/interfaces/ecpg/ecpglib/prepare.c */

#define POSTGRES_ECPG_INTERNAL
#include "postgres_fe.h"

#include <ctype.h>

#include "ecpgtype.h"
#include "ecpglib.h"
#include "ecpgerrno.h"
#include "extern.h"
#include "sqlca.h"

#define STMTID_SIZE 32

typedef struct {
    int lineno;
    char stmtID[STMTID_SIZE];
    char* ecpgQuery;
    long execs;             /* # of executions		*/
    const char* connection; /* connection for the statement		*/
} stmtCacheEntry;

static int nextStmtID = 1;
static const int stmtCacheNBuckets = 2039;  /* # buckets - a prime # */
static const int stmtCacheEntPerBucket = 8; /* # entries/bucket		*/
static stmtCacheEntry stmtCacheEntries[16384] = {{0, {0}, 0, 0, 0}};

static bool deallocate_one(int lineno, enum COMPAT_MODE c, struct connection* con, struct prepared_statement* prev,
    struct prepared_statement* thisPtr);

static bool isvarchar(unsigned char c)
{
    if (isalnum(c))
        return true;

    if (c == '_' || c == '>' || c == '-' || c == '.')
        return true;

    if (c >= 128)
        return true;

    return (false);
}

static bool replace_variables(char** text, int lineno)
{
    bool string = false;
    int counter = 1, ptr = 0;

    for (; (*text)[ptr] != '\0'; ptr++) {
        if ((*text)[ptr] == '\'')
            string = string ? false : true;

        if (string || (((*text)[ptr] != ':') && ((*text)[ptr] != '?')))
            continue;

        if (((*text)[ptr] == ':') && ((*text)[ptr + 1] == ':'))
            ptr += 2; /* skip  '::' */
        else {
            int len;
            int buffersize = sizeof(int) * CHAR_BIT * 10 / 3; /* a rough guess of the
                                                               * size we need */
            char *buffer, *newcopy;

            if ((buffer = (char*)ecpg_alloc(buffersize, lineno)) == NULL)
                return false;

            snprintf(buffer, buffersize, "$%d", counter++);

            for (len = 1; (*text)[ptr + len] && isvarchar((*text)[ptr + len]); len++)
                ;
            if ((newcopy = (char*)ecpg_alloc(strlen(*text) - len + strlen(buffer) + 1, lineno)) == NULL) {
                ecpg_free(buffer);
                return false;
            }

            strncpy(newcopy, *text, ptr);
            strcpy(newcopy + ptr, buffer);
            strcat(newcopy, (*text) + ptr + len);

            ecpg_free(*text);
            ecpg_free(buffer);

            *text = newcopy;

            if ((*text)[ptr] == '\0') /* we reached the end */
                ptr--;                /* since we will (*text)[ptr]++ in the top
                                       * level for loop */
        }
    }
    return true;
}

static bool prepare_common(int lineno, struct connection* con, const char* name, const char* variable)
{
    struct statement* stmt;
    struct prepared_statement* thisPtr;
    PGresult* query = NULL;

    /* allocate new statement */
    thisPtr = (struct prepared_statement*)ecpg_alloc(sizeof(struct prepared_statement), lineno);
    if (thisPtr == NULL)
        return false;

    stmt = (struct statement*)ecpg_alloc(sizeof(struct statement), lineno);
    if (stmt == NULL) {
        ecpg_free(thisPtr);
        return false;
    }
    /* create statement */
    stmt->lineno = lineno;
    stmt->connection = con;
    stmt->command = ecpg_strdup(variable, lineno);
    stmt->inlist = stmt->outlist = NULL;

    /* if we have C variables in our statement replace them with '?' */
    replace_variables(&(stmt->command), lineno);

    /* add prepared statement to our list */
    thisPtr->name = ecpg_strdup(name, lineno);
    thisPtr->stmt = stmt;

    /* and finally really prepare the statement */
    query = PQprepare(stmt->connection->connection, name, stmt->command, 0, NULL);
    if (!ecpg_check_PQresult(query, stmt->lineno, stmt->connection->connection, stmt->compat)) {
        ecpg_free(stmt->command);
        ecpg_free(thisPtr->name);
        ecpg_free(thisPtr);
        ecpg_free(stmt);
        return false;
    }

    ecpg_log("prepare_common on line %d: name %s; query: \"%s\"\n", stmt->lineno, name, stmt->command);
    PQclear(query);
    thisPtr->prepared = true;

    if (con->prep_stmts == NULL)
        thisPtr->next = NULL;
    else
        thisPtr->next = con->prep_stmts;

    con->prep_stmts = thisPtr;
    return true;
}

/* handle the EXEC SQL PREPARE statement */
/* questionmarks is not needed but remains in there for the time being to not change the API */
bool ECPGprepare(
    int lineno, const char* connection_name, const bool questionmarks, const char* name, const char* variable)
{
    struct connection* con;
    struct prepared_statement *thisPtr, *prev;

    (void)questionmarks; /* quiet the compiler */
    con = ecpg_get_connection(connection_name);

    if (!ecpg_init(con, connection_name, lineno))
        return false;

    /* check if we already have prepared this statement */
    thisPtr = ecpg_find_prepared_statement(name, con, &prev);
    if ((thisPtr != NULL) && !deallocate_one(lineno, ECPG_COMPAT_PGSQL, con, prev, thisPtr))
        return false;

    return prepare_common(lineno, con, name, variable);
}

struct prepared_statement* ecpg_find_prepared_statement(
    const char* name, struct connection* con, struct prepared_statement** prev_)
{
    struct prepared_statement *thisPtr, *prev;

    for (thisPtr = con->prep_stmts, prev = NULL; thisPtr != NULL; prev = thisPtr, thisPtr = thisPtr->next) {
        if (strcmp(thisPtr->name, name) == 0) {
            if (prev_ != NULL)
                *prev_ = prev;
            return thisPtr;
        }
    }
    return NULL;
}

static bool deallocate_one(int lineno, enum COMPAT_MODE c, struct connection* con, struct prepared_statement* prev,
    struct prepared_statement* thisPtr)
{
    bool r = false;

    ecpg_log("deallocate_one on line %d: name %s\n", lineno, thisPtr->name);

    /* first deallocate the statement in the backend */
    if (thisPtr->prepared) {
        char* text = NULL;
        PGresult* query = NULL;

        text = (char*)ecpg_alloc(strlen("deallocate \"\" ") + strlen(thisPtr->name), thisPtr->stmt->lineno);

        if (text != NULL) {
            sprintf(text, "deallocate \"%s\"", thisPtr->name);
            query = PQexec(thisPtr->stmt->connection->connection, text);
            ecpg_free(text);
            if (ecpg_check_PQresult(query, lineno, thisPtr->stmt->connection->connection, thisPtr->stmt->compat)) {
                PQclear(query);
                r = true;
            }
        }
    }

    /*
     * Just ignore all errors since we do not know the list of cursors we are
     * allowed to free. We have to trust the software.
     */
    if (!r && !INFORMIX_MODE(c)) {
        ecpg_raise(lineno, ECPG_INVALID_STMT, ECPG_SQLSTATE_INVALID_SQL_STATEMENT_NAME, thisPtr->name);
        return false;
    }

    /* okay, free all the resources */
    ecpg_free(thisPtr->stmt->command);
    ecpg_free(thisPtr->stmt);
    ecpg_free(thisPtr->name);
    if (prev != NULL)
        prev->next = thisPtr->next;
    else
        con->prep_stmts = thisPtr->next;

    ecpg_free(thisPtr);
    thisPtr = NULL;
    return true;
}

/* handle the EXEC SQL DEALLOCATE PREPARE statement */
bool ECPGdeallocate(int lineno, int c, const char* connection_name, const char* name)
{
    struct connection* con;
    struct prepared_statement *thisPtr, *prev;

    con = ecpg_get_connection(connection_name);

    if (!ecpg_init(con, connection_name, lineno))
        return false;

    thisPtr = ecpg_find_prepared_statement(name, con, &prev);
    if (thisPtr != NULL)
        return deallocate_one(lineno, (COMPAT_MODE)c, con, prev, thisPtr);

    /* prepared statement is not found */
    if (INFORMIX_MODE(c))
        return true;
    ecpg_raise(lineno, ECPG_INVALID_STMT, ECPG_SQLSTATE_INVALID_SQL_STATEMENT_NAME, name);
    return false;
}

bool ecpg_deallocate_all_conn(int lineno, enum COMPAT_MODE c, struct connection* con)
{
    /* deallocate all prepared statements */
    while (con->prep_stmts != NULL) {
        if (!deallocate_one(lineno, c, con, NULL, con->prep_stmts))
            return false;
    }

    return true;
}

bool ECPGdeallocate_all(int lineno, int compat, const char* connection_name)
{
    return ecpg_deallocate_all_conn(lineno, (COMPAT_MODE)compat, ecpg_get_connection(connection_name));
}

char* ecpg_prepared(const char* name, struct connection* con)
{
    struct prepared_statement* thisPtr;

    thisPtr = ecpg_find_prepared_statement(name, con, NULL);
    return thisPtr != NULL ? thisPtr->stmt->command : NULL;
}

/* return the prepared statement */
/* lineno is not used here, but kept in to not break API */
char* ECPGprepared_statement(const char* connection_name, const char* name, int lineno)
{
    (void)lineno; /* keep the compiler quiet */
    return ecpg_prepared(name, ecpg_get_connection(connection_name));
}

/*
 * hash a SQL statement -  returns entry # of first entry in the bucket
 */
static int HashStmt(const char* ecpgQuery)
{
    int stmtIx, bucketNo, hashLeng, stmtLeng;
    long long hashVal, rotVal;

    stmtLeng = strlen(ecpgQuery);
    hashLeng = 50;           /* use 1st 50 characters of statement		*/
    if (hashLeng > stmtLeng) /* if the statement isn't that long         */
        hashLeng = stmtLeng; /* use its actual length			   */

    hashVal = 0;
    for (stmtIx = 0; stmtIx < hashLeng; ++stmtIx) {
        hashVal = hashVal + (int)ecpgQuery[stmtIx];
        hashVal = hashVal << 13;
        rotVal = (hashVal & 0x1fff00000000LL) >> 32;
        hashVal = (hashVal & 0xffffffffLL) | rotVal;
    }

    bucketNo = hashVal % stmtCacheNBuckets;
    bucketNo += 1; /* don't use bucket # 0         */

    return (bucketNo * stmtCacheEntPerBucket);
}

/*
 * search the statement cache - search for entry with matching ECPG-format query
 * Returns entry # in cache if found
 *	 OR  zero if not present (zero'th entry isn't used)
 */
static int SearchStmtCache(const char* ecpgQuery)
{
    int entNo, entIx;

    /* hash the statement			*/
    entNo = HashStmt(ecpgQuery);

    /* search the cache		*/
    for (entIx = 0; entIx < stmtCacheEntPerBucket; ++entIx) {
        if (stmtCacheEntries[entNo].stmtID[0]) /* check if entry is in use		*/
        {
            if (strcmp(ecpgQuery, stmtCacheEntries[entNo].ecpgQuery) == 0)
                break; /* found it		*/
        }
        ++entNo; /* incr entry #		*/
    }

    /* if entry wasn't found - set entry # to zero  */
    if (entIx >= stmtCacheEntPerBucket)
        entNo = 0;

    return (entNo);
}

/*
 * free an entry in the statement cache
 * Returns entry # in cache used
 *	 OR  negative error code
 */
static int ecpg_freeStmtCacheEntry(int lineno, int compat, int entNo) /* entry # to free */
{
    stmtCacheEntry* entry = NULL;
    struct connection* con;
    struct prepared_statement *thisPtr, *prev;

    entry = &stmtCacheEntries[entNo];
    if (!entry->stmtID[0]) /* return if the entry isn't in use     */
        return (0);

    con = ecpg_get_connection(entry->connection);

    /* free the 'prepared_statement' list entry		  */
    thisPtr = ecpg_find_prepared_statement(entry->stmtID, con, &prev);
    if ((thisPtr != NULL) && !deallocate_one(lineno, (COMPAT_MODE)compat, con, prev, thisPtr))
        return (-1);

    entry->stmtID[0] = '\0';

    /* free the memory used by the cache entry		*/
    if (entry->ecpgQuery != NULL) {
        ecpg_free(entry->ecpgQuery);
        entry->ecpgQuery = 0;
    }

    return (entNo);
}

/*
 * add an entry to the statement cache
 * returns entry # in cache used  OR  negative error code
 */
static int AddStmtToCache(int lineno, /* line # of statement		*/
    const char* stmtID,               /* statement ID				*/
    const char* connection,           /* connection				*/
    int compat,                       /* compatibility level */
    const char* ecpgQuery)            /* query					*/
{
    int ix, initEntNo, luEntNo, entNo;
    stmtCacheEntry* entry = NULL;

    /* hash the statement																	*/
    initEntNo = HashStmt(ecpgQuery);

    /* search for an unused entry															*/
    entNo = initEntNo;   /* start with the initial entry # for the
                          * bucket	 */
    luEntNo = initEntNo; /* use it as the initial 'least used' entry			*/
    for (ix = 0; ix < stmtCacheEntPerBucket; ++ix) {
        entry = &stmtCacheEntries[entNo];
        if (!entry->stmtID[0]) /* unused entry  -	use it			*/
            break;
        if (entry->execs < stmtCacheEntries[luEntNo].execs)
            luEntNo = entNo; /* save new 'least used' entry		*/
        ++entNo;             /* increment entry #				*/
    }

    /* if no unused entries were found - use the 'least used' entry found in the bucket		*/
    if (ix >= stmtCacheEntPerBucket) /* if no unused entries were found	*/
        entNo = luEntNo;             /* re-use the 'least used' entry	*/

    /* 'entNo' is the entry to use - make sure its free										*/
    if (ecpg_freeStmtCacheEntry(lineno, compat, entNo) < 0)
        return (-1);

    /* add the query to the entry															*/
    entry = &stmtCacheEntries[entNo];
    entry->lineno = lineno;
    entry->ecpgQuery = ecpg_strdup(ecpgQuery, lineno);
    entry->connection = connection;
    entry->execs = 0;
    MemCpy(entry->stmtID, stmtID, sizeof(entry->stmtID));

    return (entNo);
}

/* handle cache and preparation of statements in auto-prepare mode */
bool ecpg_auto_prepare(int lineno, const char* connection_name, const int compat, char** name, const char* query)
{
    int entNo;

    /* search the statement cache for this statement	*/
    entNo = SearchStmtCache(query);

    /* if not found - add the statement to the cache	*/
    if (entNo) {
        char* stmtID = NULL;
        struct connection* con;
        struct prepared_statement* prep;

        ecpg_log("ecpg_auto_prepare on line %d: statement found in cache; entry %d\n", lineno, entNo);

        stmtID = stmtCacheEntries[entNo].stmtID;

        con = ecpg_get_connection(connection_name);
        prep = ecpg_find_prepared_statement(stmtID, con, NULL);
        /* This prepared name doesn't exist on this connection. */
        if ((prep == NULL) && !prepare_common(lineno, con, stmtID, query))
            return (false);

        *name = ecpg_strdup(stmtID, lineno);
    } else {
        char stmtID[STMTID_SIZE];

        ecpg_log("ecpg_auto_prepare on line %d: statement not in cache; inserting\n", lineno);

        /* generate a statement ID */
        sprintf(stmtID, "ecpg%d", nextStmtID++);

        if (!ECPGprepare(lineno, connection_name, 0, stmtID, query))
            return (false);
        if (AddStmtToCache(lineno, stmtID, connection_name, compat, query) < 0)
            return (false);

        *name = ecpg_strdup(stmtID, lineno);
    }

    /* increase usage counter */
    stmtCacheEntries[entNo].execs++;

    return (true);
}
